<template>
  <div :class="{container: true, 'view-on-hover': viewOnHover}"
       @mouseenter="onMouseEnter"
       @mouseleave="hover = false"
  >
    <div class="container-image" @transitionend="viewOnHover && !hover && (hiddenImage = true)" :style="{visibility: hiddenImage ? 'hidden' : 'visible'}">
      <slot v-if="loadError" name="error-image">
        <div class="image-fail"><span>验证码加载失败，<span class="link" @click="$emit('refresh')">点击刷新</span></span></div>
      </slot>
      <img v-else-if="imageUrl" :src="imageUrl" alt="验证码图片" @error="onLoadImageError" draggable="false" />
      <img v-if="imageUrl"
           :src="sliderUrl"
           alt=""
           class="slider"
           :style="{left: sliderLeft + 'px', top: y + 'px', visibility: loadError ? 'hidden' : 'visible'}"
           ref="slider"
           @load="onSliderImageLoad"
           @mousedown="onMouseDown"
           @error="onLoadImageError"
           draggable="false"
      />
      <div class="button-refresh" v-if="!success" @click="$emit('refresh')">
        <button-refresh-icon />
      </div>
      <div class="loading-container" v-if="loading">
        <div class="loading">
          <svg class="circular" viewBox="25 25 50 50"><circle class="path" cx="50" cy="50" r="20" fill="none"></circle></svg>
        </div>
      </div>
    </div>
    <div class="slider-bar" ref="sliderBar">
      <div class="slider-bar-tip" v-if="!dragging && sliderButtonLeft < 10">
        <span v-if="loadError">验证码加载失败, <span class="link" @click="$emit('refresh')">点击刷新</span></span>
        <span v-else-if="loading">加载中...</span>
        <span v-else-if="success">验证成功</span>
        <span v-else>向右滑动完成拼图</span>
      </div>
      <div class="slider-bar-progress" :style="{width: sliderButtonLeft + 'px'}"></div>
      <div :class="{'slider-bar-button': true, active: dragging, disabled: loadError || !sliderUrl, success}"
           @mousedown="onMouseDown"
           :style="{left: sliderButtonLeft + 'px'}"
           ref="sliderButton">
        <slot name="slider-icon">
          <slider-button-default-icon v-if="!success" />
          <slider-button-success-icon v-else />
        </slot>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import {defineComponent, h, ref, VNode, watch} from "vue";
import {faArrowRightLong, faCheck, faArrowsRotate} from "@fortawesome/free-solid-svg-icons";
import {AbstractElement, icon} from "@fortawesome/fontawesome-svg-core";
import PointInfo from "@/components/SliderCaptcha/PointInfo";

//转换fontawesome元素
const convertFontawesomeElement = (elements: AbstractElement[]): VNode[] => {
  return elements.map(e=>h(e.tag, e.attributes, convertFontawesomeElement(e.children || [])))
}
//默认图标
const sliderButtonDefaultIcon = {
  render: ()=>convertFontawesomeElement(icon(faArrowRightLong).abstract)
};
//完成图标
const sliderButtonSuccessIcon = {
  render: ()=>convertFontawesomeElement(icon(faCheck).abstract)
}
//刷新图标
const buttonRefreshIcon = {
  render: ()=>convertFontawesomeElement(icon(faArrowsRotate).abstract)
}

export default defineComponent({
  name: "SliderCaptcha",
  props: {
    //背景图片地址
    imageUrl: {
      required: true,
      type: String
    },
    //滑块图片地址
    sliderUrl: {
      required: true,
      type: String
    },
    //Y坐标
    y: {
      required: true,
      type: Number
    },
    //X坐标
    modelValue: Number,
    //是否加载中
    loading: Boolean,
    //是否鼠标移入再显示
    viewOnHover: {
      default: true,
      type: Boolean
    },
    //是否已经验证成功
    success: Boolean
  },
  emits: [
    "update:modelValue",//更新滑动位置
    "complete",//完成验证
    "refresh",//刷新
    "help",//点击帮助按钮
  ],
  components: {sliderButtonDefaultIcon, sliderButtonSuccessIcon, buttonRefreshIcon},
  setup(props, context){
    let dragging = ref(false);//拖拽中
    let startX = 0;//开始拖动位置
    let startLeft = 0;//开始拖动滑块位置
    let sliderWidth = 0;//滑块宽度
    const sliderLeft = ref(0);//滑块位置
    const sliderButtonLeft = ref(0);//滑块按钮位置
    const sliderBar = ref<HTMLDivElement>();//滑动条
    const slider = ref<HTMLDivElement>();//滑块
    const sliderButton = ref<HTMLDivElement>();//滑块按钮
    let leftmost = 0;//最左边
    let rightmost = 0;//最右边
    const loadError = ref(false);//加载失败
    const hiddenImage = ref(false);//隐藏图片, 为了展示消失过度
    const hover = ref(false);//鼠标是否在控件内
    let dragPath: PointInfo[] = [];//拖动路径

    //更新滑块位置
    const updateSliderLeft = (left: number) => {
      sliderLeft.value = left;
      const sliderButtonWidth = sliderButton.value?.clientWidth || 0;
      sliderButtonLeft.value = left + sliderWidth / 2 - sliderButtonWidth / 2;
    }

    //监听传入位置
    watch(()=>props.modelValue as number, (value: number) => {
      updateSliderLeft(value);
    });

    const onMouseDown = (e: MouseEvent) => {
      if(loadError.value || !props.sliderUrl || props.success) return;
      dragging.value = true;
      startX = e.x;
      startLeft = sliderLeft.value;
      dragPath = [{x: e.x, y: e.y, timestamp: e.timeStamp}];
      window.addEventListener("mousemove", onMouseMove);
      window.addEventListener("mouseup", onMouseUp);
    }

    const onMouseMove = (e: MouseEvent) => {
      if(!dragging.value) return;
      dragPath.push({x: e.x, y: e.y, timestamp: e.timeStamp});
      let left = startLeft + e.x - startX;
      if(left < leftmost) left = leftmost;
      if(left > rightmost){
        left = rightmost;
      }
      updateSliderLeft(left);
    }

    const onMouseUp = (e: MouseEvent) => {
      if(!dragging.value) return;
      dragging.value = false;
      window.removeEventListener("mousemove", onMouseMove);
      window.removeEventListener("mouseup", onMouseUp);
      dragPath.push({x: e.x, y: e.y, timestamp: e.timeStamp});
      context.emit("complete", sliderLeft.value, dragPath);
      context.emit("update:modelValue", sliderLeft.value);//实时触发会导致拖动卡顿
      dragPath = [];
    }

    //滑块载入后更新位置,因为滑块宽度可能不同
    const onSliderImageLoad = (e: Event) => {
      loadError.value = false;
      sliderWidth = (e.target as HTMLElement).clientWidth;
      const buttonWidth = sliderButton.value?.clientWidth || 0;
      const barWidth = sliderBar.value?.clientWidth || 0;
      leftmost = - (sliderWidth - buttonWidth) / 2;
      rightmost = barWidth - buttonWidth - (sliderWidth - buttonWidth) / 2;
      updateSliderLeft(leftmost);
    }

    //图片加载失败
    const onLoadImageError = () => {
      loadError.value = true;
    }

    //鼠标进入
    const onMouseEnter = () => {
      hover.value = true;
      hiddenImage.value = false;
    }

    /**
     * 重置
     */
    const reset = () => {
      updateSliderLeft(leftmost);
    }

    return {
      onMouseMove,
      onMouseUp,
      onMouseDown,
      onSliderImageLoad,
      onLoadImageError,
      onMouseEnter,
      sliderLeft,
      sliderBar,
      slider,
      sliderButton,
      sliderButtonLeft,
      dragging,
      loadError,
      hiddenImage,
      hover,
      reset
    }
  }
})
</script>

<style scoped>
.image-fail{
  background-color: whitesmoke;
  text-align: center;
  color: darkgray;
  height: 320px;
  line-height: 320px;
}
.slider-bar{
  position: relative;
  margin-top: .1em;
  height: 2.3em;
  background-color: #eee;
  box-shadow: inset 0 0 3px -2px #aaa;
}
.slider-bar-tip{
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  text-align: center;
  line-height: 2.3em;
  color: dimgray;
}
.slider-bar-progress{
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  background-color: #09f3;
  border: 1px solid #09f8;
}
.slider-bar-button{
  cursor: pointer;
  position: absolute;
  left: 0;
  top: 0;
  width: 3em;
  background-color: white;
  border-radius: .1em;
  line-height: 2.3em;
  box-shadow: 0 0 5px -2px gray;
  color: #666;
  user-select: none;
  transition: background-color .5s, color .5s;
}
.slider-bar-button:hover, .slider-bar-button.active, .slider-bar-button.success{
  background-color: #09f;
  color: white;
}
.slider-bar-button.disabled{
  cursor: not-allowed;
  background-color: darkgray;
  color: lightgray;
}
.slider-bar-button.success{
  cursor: default;
}
.container{
  position: relative;
}
.container.view-on-hover>.container-image{
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  opacity: 0;
  transition: opacity .5s ease-in;
}
.container.view-on-hover:hover>.container-image{
  visibility: visible;
  opacity: 1;
}
.container-image{
  position: relative;
}
.slider{
  position: absolute;
  cursor: pointer;
  user-select: none;
}
.loading-container{
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(255, 255, 255, .8);
}
.loading{
  margin-top: calc(50% - 2.8em);
  text-align: center;
}
.circular{
  width: 2.8em;
  display: inline;
  animation: loading-rotate 2s linear infinite;
}
.circular>.path{
  animation: loading-dash 1.5s ease-in-out infinite;
  stroke-dasharray: 90,150;
  stroke-dashoffset: 0;
  stroke-width: 2;
  stroke: #09f;
  stroke-linecap: round;
}
@keyframes loading-rotate {
  to {
    transform:rotate(360deg)
  }
}
@keyframes loading-dash {
  0% {
    stroke-dasharray:1,200;
    stroke-dashoffset:0
  }
  50% {
    stroke-dasharray:90,150;
    stroke-dashoffset:-40px
  }
  to {
    stroke-dasharray:90,150;
    stroke-dashoffset:-120px
  }
}
.link{
  color: #0099ff;
  cursor: pointer;
}
.button-refresh{
  position: absolute;
  top: 0;
  right: 0;
  width: 2.5em;
  height: 2.5em;
  line-height: 2.5em;
  background-color: #0005;
  color: #fff5;
  cursor: pointer;
  transition: background-color .5s, color .5s;
}
.button-refresh:hover{
  background-color: #0008;
  color: #fffe;
}
</style>
